/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.dsl.jbang.core.commands.bind;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.stream.Collectors;

import org.apache.camel.CamelException;
import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.common.JSonHelper;
import org.apache.camel.dsl.jbang.core.common.YamlHelper;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.json.Jsoner;
import picocli.CommandLine;
import picocli.CommandLine.Command;

@Command(name = "bind", description = "Bind source and sink Kamelets as a new Camel integration",
         sortOptions = false)
public class Bind extends CamelCommand {

    @CommandLine.Parameters(description = "Name of binding file to be saved", arity = "1",
                            paramLabel = "<file>", parameterConsumer = FileConsumer.class)
    Path filePath; // Defined only for file path completion; the field never used
    String file;

    @CommandLine.Option(names = { "--source" }, description = "Source (from) such as a Kamelet or Camel endpoint uri",
                        required = true)
    String source;

    @CommandLine.Option(names = { "--step" }, description = "Optional steps such as a Kamelet or Camel endpoint uri")
    String[] steps;

    @CommandLine.Option(names = { "--sink" }, description = "Sink (to) such as a Kamelet or Camel endpoint uri",
                        required = true)
    String sink;

    @CommandLine.Option(names = { "--error-handler" },
                        description = "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format \"[[apigroup/]version:]kind:[namespace/]name\", plain Camel URIs or Kamelet name.")
    String errorHandler;

    @CommandLine.Option(names = { "--property" },
                        description = "Adds a pipe property in the form of [source|sink|error-handler|step-<n>].<key>=<value> where <n> is the step number starting from 1",
                        arity = "0")
    String[] properties;

    @CommandLine.Option(names = { "--output" },
                        defaultValue = "file",
                        description = "Output format generated by this command (supports: file, yaml or json).")
    String output;

    private final TemplateProvider templateProvider;

    // Available binding providers, order in array is important!
    private final BindingProvider[] bindingProviders = new BindingProvider[] {
            new KameletBindingProvider(),
            new KnativeBrokerBindingProvider(),
            new KnativeChannelBindingProvider(),
            new StrimziKafkaTopicBindingProvider(),
            new ObjectReferenceBindingProvider(),
            new UriBindingProvider()
    };

    public Bind(CamelJBangMain main) {
        this(main, new TemplateProvider() {
        });
    }

    public Bind(CamelJBangMain main, TemplateProvider templateProvider) {
        super(main);
        this.templateProvider = templateProvider;
    }

    @Override
    public Integer doCall() throws Exception {
        String pipe = constructPipe();

        if (pipe.isEmpty()) {
            printer().println("Failed to construct Pipe resource");
            return -1;
        }

        return dumpPipe(pipe);
    }

    public String constructPipe() throws Exception {
        try {
            String sourceEndpoint = resolveEndpoint(BindingProvider.EndpointType.SOURCE, source, getProperties("source"));
            String sinkEndpoint = resolveEndpoint(BindingProvider.EndpointType.SINK, sink, getProperties("sink"));

            InputStream is = templateProvider.getPipeTemplate();
            String context = IOHelper.loadText(is);
            IOHelper.close(is);

            String stepsContext = "";
            if (steps != null) {
                StringBuilder sb = new StringBuilder("\n  steps:\n");
                for (int i = 0; i < steps.length; i++) {
                    sb.append(resolveEndpoint(BindingProvider.EndpointType.STEP, steps[i],
                            getProperties("step-%d".formatted(i + 1))));

                    if (i < steps.length - 1) {
                        sb.append("\n");
                    }
                }
                stepsContext = sb.toString();
            }

            String errorHandlerContext = "";
            if (errorHandler != null) {
                StringBuilder sb = new StringBuilder("\n  errorHandler:\n");

                Map<String, Object> errorHandlerParameters = getProperties("error-handler");

                String[] errorHandlerTokens = errorHandler.split(":", 2);
                String errorHandlerType = errorHandlerTokens[0];

                String errorHandlerSpec;
                switch (errorHandlerType) {
                    case "sink":
                        if (errorHandlerTokens.length != 2) {
                            printer().println(
                                    "Invalid error handler syntax. Type 'sink' needs an endpoint configuration (ie sink:endpointUri)");
                            // Error abort Pipe construction
                            return "";
                        }
                        String endpointUri = errorHandlerTokens[1];
                        Map<String, Object> errorHandlerSinkProperties = getProperties("error-handler.sink");

                        // remove sink properties from error handler parameters
                        errorHandlerSinkProperties.keySet().stream()
                                .map(key -> "sink." + key)
                                .filter(errorHandlerParameters::containsKey)
                                .forEach(errorHandlerParameters::remove);

                        String endpoint = resolveEndpoint(BindingProvider.EndpointType.ERROR_HANDLER, endpointUri,
                                errorHandlerSinkProperties);

                        is = templateProvider.getErrorHandlerTemplate("sink");
                        errorHandlerSpec = IOHelper.loadText(is);
                        IOHelper.close(is);
                        errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.Endpoint }}", endpoint);
                        errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerParameter }}",
                                templateProvider.asErrorHandlerParameters(errorHandlerParameters));
                        break;
                    case "log":
                        is = templateProvider.getErrorHandlerTemplate("log");
                        errorHandlerSpec = IOHelper.loadText(is);
                        IOHelper.close(is);
                        errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerParameter }}",
                                templateProvider.asErrorHandlerParameters(errorHandlerParameters));
                        break;
                    default:
                        errorHandlerSpec = "    none: {}";
                }
                sb.append(errorHandlerSpec);
                errorHandlerContext = sb.toString();
            }

            String name = FileUtil.onlyName(file, false);
            context = context.replaceFirst("\\{\\{ \\.Name }}", name);
            context = context.replaceFirst("\\{\\{ \\.Source }}\n", sourceEndpoint);
            context = context.replaceFirst("\\{\\{ \\.Sink }}\n", sinkEndpoint);
            context = context.replaceFirst("\\{\\{ \\.Steps }}", stepsContext);
            context = context.replaceFirst("\\{\\{ \\.ErrorHandler }}", errorHandlerContext);
            return context;
        } catch (Exception e) {
            printer().printErr(e);
        }

        return "";
    }

    private String resolveEndpoint(
            BindingProvider.EndpointType endpointType, String uriExpression, Map<String, Object> endpointProperties)
            throws Exception {
        for (BindingProvider provider : bindingProviders) {
            if (provider.canHandle(uriExpression)) {
                String context
                        = provider.getEndpoint(endpointType, uriExpression, endpointProperties, templateProvider);

                int additionalIndent = templateProvider.getAdditionalIndent(endpointType);
                if (additionalIndent > 0) {
                    context = Arrays.stream(context.split("\n"))
                            .map(line -> " ".repeat(additionalIndent) + line)
                            .collect(Collectors.joining("\n"));
                }

                return context;
            }
        }

        throw new CamelException(
                "Failed to resolve endpoint URI expression %s - no matching binding provider found"
                        .formatted(uriExpression));
    }

    public int dumpPipe(String pipe) throws Exception {
        switch (output) {
            case "file":
                if (file.endsWith(".yaml")) {
                    IOHelper.writeText(pipe, new FileOutputStream(file, false));
                } else if (file.endsWith(".json")) {
                    IOHelper.writeText(Jsoner.serialize(YamlHelper.yaml().loadAs(pipe, Map.class)),
                            new FileOutputStream(file, false));
                } else {
                    IOHelper.writeText(pipe, new FileOutputStream(file + ".yaml", false));
                }
                break;
            case "yaml":
                printer().println(pipe);
                break;
            case "json":
                printer().println(JSonHelper.prettyPrint(Jsoner.serialize(YamlHelper.yaml().loadAs(pipe, Map.class)), 2)
                        .replaceAll("\\\\/", "/"));
                break;
            default:
                printer().printf("Unsupported output format '%s' (supported: file, yaml, json)%n", output);
                return -1;
        }
        return 0;
    }

    /**
     * Extracts properties from given property arguments. Filter properties by given prefix. This way each component in
     * pipe (source, sink, errorHandler, step[1-n]) can have its individual properties.
     */
    private Map<String, Object> getProperties(String keyPrefix) {
        Map<String, Object> props = new HashMap<>();
        if (properties != null) {
            for (String propertyExpression : properties) {
                if (propertyExpression.startsWith(keyPrefix + ".")) {
                    String[] keyValue = propertyExpression.split("=", 2);
                    if (keyValue.length != 2) {
                        printer().printf(
                                "property '%s' does not follow format [source|sink|error-handler|step-<n>].<key>=<value>%n",
                                propertyExpression);
                        continue;
                    }

                    props.put(keyValue[0].substring(keyPrefix.length() + 1), keyValue[1]);
                }
            }
        }

        return props;
    }

    static class FileConsumer extends ParameterConsumer<Bind> {
        @Override
        protected void doConsumeParameters(Stack<String> args, Bind cmd) {
            cmd.file = args.pop();
        }
    }

    public void setFile(String file) {
        this.file = file;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public void setSink(String sink) {
        this.sink = sink;
    }

    public void setSteps(String[] steps) {
        this.steps = steps;
    }

    public void setProperties(String[] properties) {
        this.properties = properties;
    }

    public void setErrorHandler(String errorHandler) {
        this.errorHandler = errorHandler;
    }

    public void setOutput(String output) {
        this.output = output;
    }
}
